package com.kingwang.study.spring.security.demo.security.config;

import com.kingwang.study.spring.security.demo.security.constant.SecurityConstants;
import com.kingwang.study.spring.security.demo.security.filter.TokenBasedAuthenticationFilter;
import com.kingwang.study.spring.security.demo.security.handler.FormLoginAuthenticationSuccessHandler;
import com.kingwang.study.spring.security.demo.security.handler.JsonResponseAuthenticationFailureHandler;
import com.kingwang.study.spring.security.demo.security.handler.JsonResponseAuthenticationSuccessHandler;
import com.kingwang.study.spring.security.demo.security.handler.SystemLogoutHandler;
import com.kingwang.study.spring.security.demo.security.provider.TokenBasedAuthenticationProvider;
import com.kingwang.study.spring.security.demo.security.repository.HttpTokenSecurityContextRepository;
import com.kingwang.study.spring.security.demo.security.repository.SecurityContextCompositeRepository;
import com.kingwang.study.spring.security.demo.security.token.session.TokenSessionManager;
import com.kingwang.study.spring.security.demo.util.HttpServletUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;

import javax.annotation.Resource;

/**
 * Spring Security配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 拦截的URL
     */
    private static final String URL_LOGIN_TOKEN = SecurityConstants.URL_LOGIN_TOKEN;
    private static final String URL_LOGIN_FORM = SecurityConstants.URL_LOGIN_FORM;

    /**
     * 登出处理器
     */
    @Resource
    private SystemLogoutHandler systemLogoutHandler;

    /**
     * 用户信息获取服务
     */
    @Resource
    private UserDetailsService userDetailsService;

    /**
     * 密码加密
     */
    @Resource
    private PasswordEncoder passwordEncoder;

    /**
     * Token会话管理器
     */
    @Resource
    private TokenSessionManager tokenSessionManager;

    /**
     * 初始化方法
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void init(WebSecurity web) throws Exception {
        super.init(web);

        /**
         * 注册 SecurityContextRepository
         *
         * 注册的 SecurityContextRepository 对象在 SecurityContextPersistenceFilter 中使用
         *
         * 具体源码见 getHttp() -> applyDefaultConfiguration() -> http.securityContext() -> new SecurityContextConfigurer
         * () -> public void configure(H http)
         */
        SecurityContextCompositeRepository securityContextCompositeRepository =
                new SecurityContextCompositeRepository();

        securityContextCompositeRepository.addRepository(new HttpTokenSecurityContextRepository(this.tokenSessionManager));
        securityContextCompositeRepository.addRepository(new HttpSessionSecurityContextRepository());

        HttpSecurity http = getHttp();

        http.setSharedObject(SecurityContextRepository.class, securityContextCompositeRepository);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
                    // 未提供登录凭据返回HTTP 401
                    HttpServletUtils.sendHttpStatusResponse(response, HttpStatus.UNAUTHORIZED);
                })
                .and()
                .cors()
                .and()
                .csrf().disable()
                .httpBasic().disable()
                .addFilterBefore(tokenBasedAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers(URL_LOGIN_TOKEN, URL_LOGIN_FORM).permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl(URL_LOGIN_FORM)
                /**
                 * 自定义FormLogin登录成功后的处理器，不进行页面跳转，只返回 HTTP 200
                 */
                .successHandler(new FormLoginAuthenticationSuccessHandler())
                .and()
                .logout()
                /**
                 * 自定义登出处理器，处理Session和Token失效
                 */
                .addLogoutHandler(this.systemLogoutHandler)
                /**
                 * 登出成功后返回HTTP 200
                 */
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         * 添加AuthenticationProvider，注意顺序
         * 一旦使用了这个方法指定AuthenticationProvider，需要同时指定用于Token和FormLogin的Provider
         */
        auth.authenticationProvider(tokenBasedAuthenticationProvider())
                .authenticationProvider(daoAuthenticationProvider());
    }

    /**
     * 自定义TokenBasedAuthenticationFilter
     *
     * @return
     * @throws Exception
     */
    @Bean
    public TokenBasedAuthenticationFilter tokenBasedAuthenticationFilter() throws Exception {
        TokenBasedAuthenticationFilter result = new TokenBasedAuthenticationFilter();

        result.setAuthenticationManager(authenticationManager());
        /**
         * Token认证登录成功和失败后返回JSON
         */
        result.setAuthenticationSuccessHandler(jsonResponseAuthenticationSuccessHandler());
        result.setAuthenticationFailureHandler(jsonResponseAuthenticationFailureHandler());

        return result;
    }

    @Bean
    AuthenticationSuccessHandler jsonResponseAuthenticationSuccessHandler() {
        return new JsonResponseAuthenticationSuccessHandler();
    }

    @Bean
    AuthenticationFailureHandler jsonResponseAuthenticationFailureHandler() {
        return new JsonResponseAuthenticationFailureHandler();
    }

    private AuthenticationProvider tokenBasedAuthenticationProvider() {
        TokenBasedAuthenticationProvider result = new TokenBasedAuthenticationProvider();

        result.setUserDetailsService(this.userDetailsService);
        result.setPasswordEncoder(this.passwordEncoder);
        result.setTokenSessionManager(this.tokenSessionManager);

        return result;
    }

    @Override
    protected UserDetailsService userDetailsService() {
        return this.userDetailsService;
    }

    private AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider result = new DaoAuthenticationProvider();

        result.setUserDetailsService(this.userDetailsService);
        result.setPasswordEncoder(this.passwordEncoder);

        return result;
    }
}
